<html>
<head>
  <title>20.状态（State）模式</title>
  <basefont face="微软雅黑" size="2" />
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <meta name="exporter-version" content="YXBJ Windows/602628 (zh-CN, DDL); Windows/10.0.0 (Win64); EDAMVersion=V2;"/>
  <style>
    body, td {
      font-family: 微软雅黑;
      font-size: 10pt;
    }
  </style>
</head>
<body>
<a name="26168"/>
<h1>20.状态（State）模式</h1>
<div>
<table bgcolor="#D4DDE5" border="0">
<tr><td><b>创建时间：</b></td><td><i>2020/9/30 7:24</i></td></tr>
<tr><td><b>更新时间：</b></td><td><i>2021/1/27 6:30</i></td></tr>
<tr><td><b>作者：</b></td><td><i>YuGe</i></td></tr>
</table>
</div>
<br/>

<div>
<span><div><div><span style="font-size: 12pt; font-weight: bold;">一、基本描述</span></div><ul><li><div><span style="font-size: 16px;">允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。</span></div></li><li><div><span style="font-size: 16px;">又叫做状态对象（object for state）</span></div></li><li><div><span style="font-size: 16px;">对于定义的理解：</span></div></li><ul><li><div><span style="font-size: 16px;">允许一个对象在其内部状态改变时改变它的行为</span></div><div><span style="font-size: 16px;">因为这个模式将状态封装成为独立的类，并将动作委托到代表当前状态的对象，此时行为会随着内部状态而改变</span></div></li><li><div><span style="font-size: 16px;">对象看起来似乎修改了它的类</span></div><div><span style="font-size: 16px;">从客户的视角来看：如果说你使用的对象能够完全改变它的行为，那么你会觉得，这个对象实际上是从别的类实例化而来的。然而，实际上，我们是在使用组合通过简单引用不同的状态对象来造成类改变的假象。</span></div></li></ul><li><div><span style="font-size: 16px;">适用性：</span></div></li><ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">行为随状态改变而改变的场景。</span></span><span style="font-size: 16px;">一个对象的行为取决于它的状态，并且它必须在运行时刻根据状态改变它的行为。</span></div></li><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">条件、分支语句的替代者。</span>一个操作中含有庞大的多分支条件语句，且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常，有多个操作包含这一相同的条件结构。状态模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象，这一对象可以不依赖于其他对象而独立变化。（实际上就是将原本的一大堆的 if-else 语句换成了 state 模式，增加了扩展性和可维护性）</span></div></li></ul><li><div><span style="font-size: 16px;">对于实现的考虑：</span></div></li><ul><li><div><span style="font-size: 16px; font-weight: bold;">State 模式不指定哪一个参与者定义状态转换规则。</span></div></li><ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">统一由环境类来负责状态之间的转换【</span></span><span style="font-size: 16px;"><span style="font-size: 16px; color: unset; font-family: unset; font-style: italic;">Context </span></span><span style="font-size: 16px; color: unset; font-family: unset; font-style: italic;">对象切换 ConcreteState 类</span><span style="font-size: 16px; font-weight: bold;">】</span><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">。</span>如果该规则是固定的，那么它们可在 Context 中完全实现。</span> <span style="font-size: 12pt;">此时，环境类还充当了状态管理器（State Manager</span><span style="font-size: 12pt;">）</span><span style="font-size: 12pt;">角</span><span style="font-size: 12pt; color: unset; font-family: unset;">色， 在环境类的业务方法中通过对某些属性值的判断实现状态转换， 还可以提供一个专门的</span><span style="font-size: 12pt; color: unset; font-family: unset;">方法用于实现属性判断和状态转换。</span></div></li><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">由具体的状态类来负责状态之间的转换【</span></span><span style="font-size: 16px; color: unset; font-family: unset; font-style: italic;">State 对象自身切换类</span><span style="font-size: 16px; font-weight: bold;">】</span><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">。</span></span><span style="font-size: 12pt;">可以在具体状态类的业务方法中判断环境类的某些</span><span style="font-size: 12pt; color: unset; font-family: unset;">属性值再根据情况为环境类设置新的状态对象，实现状态转换，同样，也可以提供一个专门</span><span style="font-size: 12pt; color: unset; font-family: unset;">的方法来负责属性值的判断和状态转换<span style="font-size: 12pt; color: unset; font-family: unset; font-weight: bold;">。</span></span><span style="font-size: 16px; color: unset; font-family: unset;">这需要 Context 增加一个接口，让 State 对象显式的设定 Context 的当前状态。</span></div></li></ul></ul></ul><div style="margin-left: 120px;"><span style="font-size: 16px; color: unset; font-family: unset;">若让 State 子类自身指定它们的后继状态以及何时进行转换，通常更灵活、更合适。</span><span style="font-size: 12pt; color: unset; font-family: unset;">用这种方法分散转换逻辑可以很容易地定义新的 State 子类来修改和扩展该逻辑。</span></div><div style="margin-left: 120px;"><span style="font-size: 12pt; color: unset; font-family: unset;">这样做的一个缺点是，一个 State 子类至少拥有一个其他子类的信息，这就在各子类之间产生了实现依赖。同时，</span><span style="font-size: 12pt;">状态类与环境类之间就将存在依赖或关联关</span><span style="font-size: 12pt; color: unset; font-family: unset;">系，因为状态类需要访问环境类中的属性值。</span></div><ul><ul><li><div><span style="font-size: 16px; font-weight: bold;">创建和销毁 State 对象的时机。</span><span style="font-size: 16px;">需要考量究竟是仅当需要 State 对象时才创建它们并随后销毁它们（</span><span style="font-size: 16px; font-style: italic;">转换到本状态的时候临时创建，转换到别的状态的时候销毁</span><span style="font-size: 16px;">），还是提前创建它们并且始终不销毁它们（</span><span style="font-size: 16px; font-style: italic;">在构造函数当中或者全局初始化所有的状态对象，状态对象一直不销毁，生命周期与程序的生命周期相同</span><span style="font-size: 16px;">）。</span></div></li></ul></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">当将要进入的状态在运行时是不可知的，并且上下文不经常改变状态时，第一种选择较为可取。这种方法避兔创建不会被用到的对象，如果 State 对象存储大量的信息时这一点很重要。当状态改变很频繁时，第二种方法较好。在这种情况下最好避免销毁状态，因为可能很快再次需要用到它们。此时可以预先一次付清创建各个状态对象的开销，并且在运行过程中根本不存在销毁状态对象的开销。但是这种方法可能不太方便，因为 Context 必须保存对所有可能会进入的那些状态的引用。（</span><span style="font-size: 12pt; font-style: italic;">实际上是一个时空权衡问题</span><span style="font-size: 12pt;">）</span></div><ul><li><div><span style="font-size: 16px;">状态对象通常是单例。</span></div></li><li><div><span style="font-size: 16px;">状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况，把状态的判断逻辑转移到表示不同状态的一系列类当中，可以把复杂的判断逻辑简化。</span></div></li></ul><div style="margin-left: 40px;"><span style="font-size: 16px;">如果方法很长，且有很多判断分支，意味着它责任过大，是坏代码的味道。面向对象设计其实就是希望做到代码的责任分解，需要将这些分支变成多个类，增加时不影响其他的类，然后状态变化在各自的类中完成。</span></div><ul><li><div><span style="font-size: 16px;">状态模式的目的是为了消除庞大的条件分支语句，大的分支判断会使得它们难以修改和扩展，状态模式通过把各种状态转移逻辑分布到 State 的子类之间，来减少相互间的依赖。</span></div></li><li><div><span style="font-size: 16px;">对于每一个状态类，需要实现在一个状态下的两个任务模型：这个状态是如何产生的，在这个状态下还能做什么其他动作（即这个状态怎么过渡到其他状态）。</span></div></li><li><div><span style="font-size: 12pt;">状态模式的核心是封装， 状态的变更引起了行为的变更， 从外部看起来就好像这个对象</span><span style="font-size: 12pt; color: unset; font-family: unset;">对应的类发生了改变一样。</span></div></li><li><div><span style="font-size: 12pt;">状态模式相对来说比较复杂， 它提供了一种对物质运动的另一个观察视角， 通过状态变</span><span style="font-size: 12pt; color: unset; font-family: unset;">更促使行为的变化。</span></div></li><li><div><span style="font-size: 12pt;">状态模式隐藏了状态的变化过程，它的切换引起了行为的变化。对外来说，</span><span style="font-size: 12pt; color: unset; font-family: unset;">只看到行为的发生改变，而不用知道是状态变化引起的。</span></div></li><li><div><span style="font-size: 16px;">注意事项：</span> <span style="font-size: 12pt;">状态模式适用于当某个对象在它的状态发生改变时， 它的行为也随着发生比较大的变</span><span style="font-size: 12pt; color: unset; font-family: unset;">化， 也就是说在行为受状态约束的情况下可以使用状态模式， 而且使用时对象的状态最好不</span><span style="font-size: 12pt; color: unset; font-family: unset;">要超过5个。</span></div></li><li><div><span style="font-size: 16px;">状态模式用于描述对象状态及其转换。</span></div></li><li><div><span style="font-size: 12pt;">在状态模式中， 我们将对象在每一个状态下的</span><span style="font-size: 12pt; color: unset; font-family: unset;">行为和状态转移语句封装在一个个状态类中， 通过这些状态类来分散冗长的条件转移语句，</span><span style="font-size: 12pt; color: unset; font-family: unset;">让系统具有更好的灵活性和可扩展性。</span></div></li><li><div><span style="font-size: 12pt;">状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。 当系统中某</span><span style="font-size: 12pt; color: unset; font-family: unset;">个对象存在多个状态， 这些状态之间可以进行转换， 而且对象在不同状态下行为不相同时可</span><span style="font-size: 12pt; color: unset; font-family: unset;">以使用状态模式。 状态模式将一个对象的状态从该对象中分离出来， 封装到专门的状态类</span><span style="font-size: 12pt; color: unset; font-family: unset;">中， 使得对象状态可以灵活变化， 对于客户端而言， 无须关心对象状态的转换以及对象所处</span><span style="font-size: 12pt; color: unset; font-family: unset;">的当前状态， 无论对于何种状态的对象， 客户端都可以一致处理。</span></div></li><li><div><span style="font-size: 12pt;">状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中，通过设置不同的状态</span><span style="font-size: 12pt; color: unset; font-family: unset;">对象可以让环境对象拥有不同的行为，而状态转换的细节对于客户端而言是透明的，方便了</span><span style="font-size: 12pt; color: unset; font-family: unset;">客户端的使用。</span></div></li><li><div><span style="font-size: 16px;">遵守“封装变化”原则：局部化每个状态的行为，这样一来，如果针对某个状态做了改变，就不会把其他的代码搞乱了。</span></div></li><li><div><span style="font-size: 16px;">实际应用：</span></div></li><ul><li><div><span style="font-size: 16px;">实际中的状态模式，大多均需要实现状态间的自由切换。</span></div></li></ul></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">【</span><span style="font-size: 12pt; font-style: italic;">初步理解实现状态之间自由切换需要在 State 抽象类中合理设置接口：每当有一个新的状态，就需要在 State 当中有一个新的接口与之对应，这一点在《GOF》的TCP实例和《设计模式之禅》中的示例中有体现，也就是说在 State 中有全量的接口，而在 ConcreteState 中只实现（重载）与之相关的接口，其余接口沿用 State 类中的默认实现（即无关接口仍然在 ConcreteState 当中存在，但不会有行为及状态转换）</span><span style="font-size: 12pt;">】</span></div><ul><ul><li><div><span style="font-size: 12pt;">在有些情况下， 多个环境对象可能需要共享同一个状态， 如果希望在系统中实现多个环境对</span><span style="font-size: 12pt; color: unset; font-family: unset;">象共享一个或多个状态对象， 那么需要将这些状态对象定义为环境类的静态成员对象。【</span><span style="font-size: 12pt; color: unset; font-family: unset; font-style: italic;">多个环境对象是同一个 Context 类的不同实例</span><span style="font-size: 12pt; color: unset; font-family: unset;">】【</span><span style="font-size: 12pt; color: unset; font-family: unset; font-style: italic;">另外这种情况也可以结合 Flyweight 享元模式混合实现</span><span style="font-size: 12pt; color: unset; font-family: unset;">】</span></div></li><li><div><span style="font-size: 16px;">状态模式在工作流和游戏开发中都得到了广泛的应用，工作流开发中的状态机管理，实际上就是状态模式中的 Context 类的升级，公文状态的转换和游戏中角色的升级也使用到了状态模式。</span></div></li></ul></ul><div><span style="font-size: 12pt; font-weight: bold;">二、图例</span></div><div style="text-align: center;"><img src="20.状态（State）模式_files/Image.png" type="image/png" data-filename="Image.png"/></div><ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">Context（环境）</span>：又称为上下文类。定义客户感兴趣的接口。维护一个 ConcreteState 子类的实例，这个实例定义当前状态。客户实际上使用此类，负责具体状态的切换，作用是串联各个状态的过渡。环境类实际上是真正拥有状态的对象，我们只是将环境类中与状态有关的代码提取出来封装到专门的状态类中。作为 Context 类有两个约束：</span></div></li><ul><li><div><span style="font-size: 16px;">把状态对象声明为静态常量，有几个状态对象就声明几个静态常量。</span></div></li><li><div><span style="font-size: 16px;">环境角色具有状态抽象角色定义的所有行为，具体执行使用委托方式。</span></div></li></ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">State（状态）</span>：定义一个接口以封装与 Context 的一个特定状态相关的行为。对各状态下的共有行为进行封装。接口或抽象类，负责对象状态的定义，并且封装环境角色以实现状态切换。</span></div></li><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">ConcreteState subclasses（具体状态子类）</span>：每一个子类实现一个与 Context 的一个状态相关的行为。有两个职责：</span></div></li><ul><li><div><span style="font-size: 16px;">本状态行为的管理（</span><span style="font-size: 16px; font-style: italic;">本状态下要做的事</span><span style="font-size: 16px;">）。</span></div></li><li><div><span style="font-size: 16px;">趋向状态的处理（</span><span style="font-size: 16px; font-style: italic;">本状态如何过渡到其他状态</span><span style="font-size: 16px;">）。</span></div></li></ul><li><div><span style="font-size: 12pt; font-weight: bold;">协作：</span></div></li><ul><li><div><span style="font-size: 16px;">Context 将与状态相关的请求委托给当前的 ConcreteState 对象处理。</span></div></li><li><div><span style="font-size: 16px;">Context 可将自身作为一个参数传递给处理该请求的状态对象，这使得状态对象在必要时可访问 Context。</span></div></li><li><div><span style="font-size: 16px;">Context 是客户使用的主要接口。客户可用状态对象来配置一个 Context，一旦一个 Context 配置完毕，它的客户不再需要直接与状态对象打交道。</span></div></li><li><div><span style="font-size: 12pt;">Context 与 ConcreteState 子类都可决定哪个状态是另外一个的后继者，以及是在何种条件下进行状态转换。（</span><span style="font-size: 12pt; font-style: italic;">实际上就是一个有限状态机</span><span style="font-size: 12pt;">）</span></div></li><li><div><span style="font-size: 16px;">Context和C</span><span style="font-size: 16px; color: unset; font-family: unset;">oncreteState都可以决定状态转换的流向</span></div><div><span style="font-size: 16px;">当状态转换时固定的时候，就适合放在Context中，然而，当转换时更动态的时候，通常会放在状态类中。</span></div><div><span style="font-size: 16px;">将状态转换放在状态类中的缺点是：状态类之间产生了依赖。</span></div><div><span style="font-size: 16px;">做这个决策的同时，也等于是在为另一件事情做决策：当系统进化时，究竟哪个类是对修改封闭（Context还是ConcreteState）的。</span></div></li></ul></ul><div><span style="font-size: 12pt; font-weight: bold;">三、优缺点</span></div><div><span style="font-size: 12pt; font-weight: bold;">优点</span></div><ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">将与特定状态相关的行为局部化，并且将不同状态的行为分割开来。</span>State 模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个 State 子类中，所以通过定义新的子类可以很容易的增加新的状态和转换。</span></div></li></ul><div style="margin-left: 40px;"><span style="font-size: 12pt;">另一个方法是使用数据值定义内部状态并且让 Context 操作来显式地检查这些数据。但这样将会使整个 Context 的实现中遍布看起来很相似的条件语句或 case 语句。增加一个新的状态可能需要改变若干个操作，这就使得维护变得复杂了。</span></div><div style="margin-left: 40px;"><span style="font-size: 16px;">State 模式避免了这个问题，但可能会引入另一个问题，因为该模式将不同状态的行为分布在多个 State 子类中。这就增加了子类的数目，相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些，否则需要使用巨大的条件语句。</span></div><div style="margin-left: 40px;"><span style="font-size: 12pt;">正如很长的过程一样，巨大的条件语句是不受欢迎的。它们形成一大整并且使得代码不够清晰，这又使得它们难以修改和扩展。 State 模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的 if 或  switch 语句中，而是分布在 State 子类之间。</span><span style="font-size: 12pt; color: unset; font-family: unset;">将每一个状态转换和动作封装到一个类中，就把着眼点从执行状态提高到整个对象的状态。</span><span style="font-size: 12pt; color: unset; font-family: unset;">这将使代码结构化并使其意图更加清晰。</span></div><div style="margin-left: 40px;"><span style="font-size: 16px;">同时，</span><span style="font-size: 12pt;">状态变换放置到类的内部来实现，外部的调用不用知道类</span><span style="font-size: 12pt; color: unset; font-family: unset;">内部如何实现状态和行为的变换。</span></div><ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">使得状态转换显示化</span>。当一个对象</span><span style="font-size: 12pt;">仅以内部数据值来定义当前状态时，其状态仅表现为对一些变量的赋值，这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。</span><span style="font-size: 12pt; color: unset; font-family: unset;">而且， State 对象可保证 Context 不会发生内部状态不一致的情况，因为从 Context 的角度看，状态转换是原子的 —— 只需重新绑定一个变量（即 Context 的 State 对象变量），而无需为多个变量赋值。</span></div></li><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">State 对象可被共享</span>。</span><span style="font-size: 12pt;">如果 State 对象没有实例变量 —— 它们表示的状态完全以它们的类型来编码 —— 那么各 Context 对象可以共享一个 State 对象。当状态以这种方式被共享时，它们必然是没有内部状态只有行为的轻量级对象（可参考 Flyweight 模式）。（</span><span style="font-size: 12pt; font-style: italic;">这一条就是说各子 ConcreteState 可以没有内部成员，只有方法，其类就是状态的代表</span><span style="font-size: 12pt;">）</span></div></li><li><div><span style="font-size: 16px;">很好的体现了</span><span style="font-size: 12pt; font-weight: bold;">单一职责原则</span><span style="font-size: 12pt;">，每个状态都是一个子类。</span></div></li><li><div><span style="font-size: 12pt;">可以让多个环境对象共享一个状态对象，从而减少系统中对象的个数。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">缺点</span></div><ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px; font-weight: bold;">子类可能太多，可能发生类膨胀。</span>一个事物可能有很多个状态，完全使用状态模式会有很多子类，不好管理，导致系统运行开销增大。应在项目中做实际的权衡。</span></div></li><li><div><span style="font-size: 12pt;">状态模式的结构与实现都较为复杂，如果使用不当将导致程序结构和代码的混乱，增加系</span><span style="font-size: 12pt; color: unset; font-family: unset;">统设计的难度。</span></div></li><li><div><span style="font-size: 12pt;">状态模式对</span><span style="font-size: 12pt; font-weight: bold;">开闭原则</span><span style="font-size: 12pt;">的支持并不太好，</span><span style="font-size: 12pt;">如要增加状态就要增</span><span style="font-size: 12pt; color: unset; font-family: unset;">加子类，</span><span style="font-size: 12pt;">增加新的状态类需要修改那些负责状态转换的源</span><span style="font-size: 12pt; color: unset; font-family: unset;">代码，否则无法转换到新增状态；而且修改某个状态类的行为也需修改对应类的源代码。</span></div></li></ul><div style="margin-left: 40px;"><span style="font-size: 12pt;">【<span style="font-size: 12pt; font-style: italic;">关于对开闭原则的支持，《设计模式之禅》和《Design Pattern Java》两本书描述的不一样，</span></span><span style="font-size: 12pt; font-style: italic;">《设计模式之禅》认为状态模式支持开闭原则，这是优点，但是</span><span style="font-size: 12pt; font-style: italic;">《Design Pattern Java》认为状态模式对开闭原则支持的不够好，这是缺点。这里采用</span><span style="font-size: 12pt; font-style: italic;">《Design Pattern Java》当中的描述</span><span style="font-size: 12pt;">】</span></div><div><span style="font-size: 12pt; font-weight: bold;">四、可能的改进</span></div><ul><li><div><span style="font-size: 16px;">结合生成器模式（Builder）会起到非常好的封装作用。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">五、个人理解</span></div><ul><li><div><span style="font-size: 16px;">状态模式的概念与实现相对而言比较复杂</span></div></li><li><div><span style="font-size: 16px;">状态模式的应用似乎很广泛，在实际中有很多状态相关的编码或各种条件语句的长代码可用状态模式实现。</span></div></li><li><div><span style="font-size: 12pt;">状态模式的应用场景认识比较统一：1）状态相关的场景，或者2）包含大量条件语句的场景（但是包含大量条件语句的场景也可以用策略模式替代，所以还是要认准状态相关的场景）</span></div></li><li><div><span style="font-size: 16px;">状态模式和策略模式的区别：个人认为状态模式和策略模式非常相似，最主要的区别大概是状态模式中要关心状态的转换，而在策略模式中，需要关心策略的选用（不用转换）</span></div></li></ul><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><font style="font-size: 12pt;"><br clear="none"/></font></div><div><br/></div></div><div><br/></div></span>
</div></body></html> 